Slovenčina

Preskúmajte 'branded types' v TypeScripte, mocnú techniku na dosiahnutie nominálneho typovania v štrukturálnom typovom systéme. Zistite, ako zvýšiť typovú bezpečnosť a prehľadnosť kódu.

TypeScript Branded Types: Nominálne Typovanie v Štrukturálnom Systéme

Štrukturálny typový systém TypeScriptu ponúka flexibilitu, ale niekedy môže viesť k neočakávanému správaniu. 'Branded types' poskytujú spôsob, ako vynútiť nominálne typovanie, čím sa zvyšuje typová bezpečnosť a prehľadnosť kódu. Tento článok podrobne preskúma 'branded types', poskytne praktické príklady a osvedčené postupy pre ich implementáciu.

Pochopenie Štrukturálneho vs. Nominálneho Typovania

Predtým, ako sa ponoríme do 'branded types', objasnime si rozdiel medzi štrukturálnym a nominálnym typovaním.

Štrukturálne Typovanie (Duck Typing)

V štrukturálnom typovom systéme sú dva typy považované za kompatibilné, ak majú rovnakú štruktúru (t.j. rovnaké vlastnosti s rovnakými typmi). TypeScript používa štrukturálne typovanie. Zvážte tento príklad:


interface Point {
  x: number;
  y: number;
}

interface Vector {
  x: number;
  y: number;
}

const point: Point = { x: 10, y: 20 };
const vector: Vector = point; // Valid in TypeScript

console.log(vector.x); // Output: 10

Aj keď sú Point a Vector deklarované ako odlišné typy, TypeScript umožňuje priradiť objekt Point premennej Vector, pretože majú rovnakú štruktúru. To môže byť pohodlné, ale môže to tiež viesť k chybám, ak potrebujete rozlišovať medzi logicky odlišnými typmi, ktoré majú náhodou rovnaký tvar. Napríklad, ak si predstavíme súradnice zemepisnej šírky/dĺžky, ktoré sa môžu náhodne zhodovať so súradnicami pixelov na obrazovke.

Nominálne Typovanie

V nominálnom typovom systéme sú typy považované za kompatibilné iba vtedy, ak majú rovnaký názov. Aj keď majú dva typy rovnakú štruktúru, považujú sa za odlišné, ak majú rôzne názvy. Jazyky ako Java a C# používajú nominálne typovanie.

Potreba 'Branded Types'

Štrukturálne typovanie v TypeScripte môže byť problematické, keď potrebujete zabezpečiť, aby hodnota patrila ku konkrétnemu typu bez ohľadu na jej štruktúru. Zoberme si napríklad reprezentáciu mien. Môžete mať rôzne typy pre USD a EUR, ale obe by mohli byť reprezentované ako čísla. Bez mechanizmu na ich rozlíšenie by ste mohli omylom vykonávať operácie s nesprávnou menou.

'Branded types' riešia tento problém tým, že vám umožňujú vytvárať odlišné typy, ktoré sú štrukturálne podobné, ale typový systém ich považuje za rozdielne. To zvyšuje typovú bezpečnosť a predchádza chybám, ktoré by inak mohli prekĺznuť.

Implementácia 'Branded Types' v TypeScripte

'Branded types' sa implementujú pomocou prienikových typov (intersection types) a jedinečného symbolu alebo reťazcového literálu. Myšlienkou je pridať k typu "značku" (brand), ktorá ho odlíši od iných typov s rovnakou štruktúrou.

Použitie Symbolov (Odporúčané)

Používanie symbolov na 'branding' je vo všeobecnosti preferované, pretože symboly sú zaručene jedinečné.


const USD = Symbol('USD');
type USD = number & { readonly [USD]: unique symbol };

const EUR = Symbol('EUR');
type EUR = number & { readonly [EUR]: unique symbol };

function createUSD(value: number): USD {
  return value as USD;
}

function createEUR(value: number): EUR {
  return value as EUR;
}

function addUSD(a: USD, b: USD): USD {
  return (a + b) as USD;
}

const usd1 = createUSD(10);
const usd2 = createUSD(20);
const eur1 = createEUR(15);

const totalUSD = addUSD(usd1, usd2);
console.log("Total USD:", totalUSD);

// Uncommenting the next line will cause a type error
// const invalidOperation = addUSD(usd1, eur1);

V tomto príklade sú USD a EUR 'branded types' založené na type number. unique symbol zaisťuje, že tieto typy sú odlišné. Funkcie createUSD a createEUR sa používajú na vytváranie hodnôt týchto typov a funkcia addUSD prijíma iba hodnoty USD. Pokus o pripočítanie hodnoty EUR k hodnote USD spôsobí typovú chybu.

Použitie Reťazcových Literálov

Na 'branding' môžete použiť aj reťazcové literály, hoci tento prístup je menej robustný ako použitie symbolov, pretože reťazcové literály nie sú zaručene jedinečné.


type USD = number & { readonly __brand: 'USD' };
type EUR = number & { readonly __brand: 'EUR' };

function createUSD(value: number): USD {
  return value as USD;
}

function createEUR(value: number): EUR {
  return value as EUR;
}

function addUSD(a: USD, b: USD): USD {
  return (a + b) as USD;
}

const usd1 = createUSD(10);
const usd2 = createUSD(20);
const eur1 = createEUR(15);

const totalUSD = addUSD(usd1, usd2);
console.log("Total USD:", totalUSD);

// Uncommenting the next line will cause a type error
// const invalidOperation = addUSD(usd1, eur1);

Tento príklad dosahuje rovnaký výsledok ako predchádzajúci, ale používa reťazcové literály namiesto symbolov. Aj keď je to jednoduchšie, je dôležité zabezpečiť, aby boli reťazcové literály použité na 'branding' jedinečné v rámci vašej kódovej základne.

Praktické Príklady a Prípady Použitia

'Branded types' možno aplikovať na rôzne scenáre, kde potrebujete vynútiť typovú bezpečnosť nad rámec štrukturálnej kompatibility.

Identifikátory (ID)

Zvážte systém s rôznymi typmi identifikátorov, ako sú UserID, ProductID a OrderID. Všetky tieto ID môžu byť reprezentované ako čísla alebo reťazce, ale chcete zabrániť náhodnému zamieňaniu rôznych typov ID.


const UserIDBrand = Symbol('UserID');
type UserID = string & { readonly [UserIDBrand]: unique symbol };

const ProductIDBrand = Symbol('ProductID');
type ProductID = string & { readonly [ProductIDBrand]: unique symbol };

function getUser(id: UserID): { name: string } {
  // ... fetch user data
  return { name: "Alice" };
}

function getProduct(id: ProductID): { name: string, price: number } {
  // ... fetch product data
  return { name: "Example Product", price: 25 };
}

function createUserID(id: string): UserID {
  return id as UserID;
}

function createProductID(id: string): ProductID {
  return id as ProductID;
}

const userID = createUserID('user123');
const productID = createProductID('product456');

const user = getUser(userID);
const product = getProduct(productID);

console.log("User:", user);
console.log("Product:", product);

// Uncommenting the next line will cause a type error
// const invalidCall = getUser(productID);

Tento príklad ukazuje, ako môžu 'branded types' zabrániť odovzdaniu ProductID funkcii, ktorá očakáva UserID, čím sa zvyšuje typová bezpečnosť.

Doménovo-Špecifické Hodnoty

'Branded types' môžu byť užitočné aj na reprezentáciu doménovo-špecifických hodnôt s obmedzeniami. Napríklad môžete mať typ pre percentá, ktoré by mali byť vždy medzi 0 a 100.


const PercentageBrand = Symbol('Percentage');
type Percentage = number & { readonly [PercentageBrand]: unique symbol };

function createPercentage(value: number): Percentage {
  if (value < 0 || value > 100) {
    throw new Error('Percentage must be between 0 and 100');
  }
  return value as Percentage;
}

function applyDiscount(price: number, discount: Percentage): number {
  return price * (1 - discount / 100);
}

try {
  const discount = createPercentage(20);
  const discountedPrice = applyDiscount(100, discount);
  console.log("Discounted Price:", discountedPrice);

  // Uncommenting the next line will cause an error during runtime
  // const invalidPercentage = createPercentage(120);
} catch (error) {
  console.error(error);
}

Tento príklad ukazuje, ako vynútiť obmedzenie na hodnotu 'branded type' počas behu programu. Hoci typový systém nemôže zaručiť, že hodnota Percentage je vždy medzi 0 a 100, funkcia createPercentage môže toto obmedzenie vynútiť počas behu. Môžete tiež použiť knižnice ako io-ts na vynútenie validácie 'branded types' za behu.

Reprezentácie Dátumu a Času

Práca s dátumami a časmi môže byť zložitá kvôli rôznym formátom a časovým pásmam. 'Branded types' môžu pomôcť rozlíšiť medzi rôznymi reprezentáciami dátumu a času.


const UTCDateBrand = Symbol('UTCDate');
type UTCDate = string & { readonly [UTCDateBrand]: unique symbol };

const LocalDateBrand = Symbol('LocalDate');
type LocalDate = string & { readonly [LocalDateBrand]: unique symbol };

function createUTCDate(dateString: string): UTCDate {
  // Validate that the date string is in UTC format (e.g., ISO 8601 with Z)
  if (!/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/.test(dateString)) {
    throw new Error('Invalid UTC date format');
  }
  return dateString as UTCDate;
}

function createLocalDate(dateString: string): LocalDate {
  // Validate that the date string is in local date format (e.g., YYYY-MM-DD)
  if (!/\d{4}-\d{2}-\d{2}/.test(dateString)) {
    throw new Error('Invalid local date format');
  }
  return dateString as LocalDate;
}

function convertUTCDateToLocalDate(utcDate: UTCDate): LocalDate {
  // Perform time zone conversion
  const date = new Date(utcDate);
  const localDateString = date.toLocaleDateString();
  return createLocalDate(localDateString);
}

try {
  const utcDate = createUTCDate('2024-01-20T10:00:00.000Z');
  const localDate = convertUTCDateToLocalDate(utcDate);
  console.log("UTC Date:", utcDate);
  console.log("Local Date:", localDate);
} catch (error) {
  console.error(error);
}

Tento príklad rozlišuje medzi UTC a lokálnymi dátumami, čím zaisťuje, že v rôznych častiach vašej aplikácie pracujete so správnou reprezentáciou dátumu a času. Validácia za behu zaisťuje, že týmto typom môžu byť priradené iba správne naformátované reťazce dátumu.

Osvedčené Postupy pre Používanie 'Branded Types'

Ak chcete efektívne používať 'branded types' v TypeScripte, zvážte nasledujúce osvedčené postupy:

Výhody 'Branded Types'

Nevýhody 'Branded Types'

Alternatívy k 'Branded Types'

Hoci sú 'branded types' mocnou technikou na dosiahnutie nominálneho typovania v TypeScripte, existujú alternatívne prístupy, ktoré by ste mohli zvážiť.

Nepriehľadné Typy (Opaque Types)

Nepriehľadné typy sú podobné 'branded types', ale poskytujú explicitnejší spôsob, ako skryť podkladový typ. TypeScript nemá vstavanú podporu pre nepriehľadné typy, ale môžete ich simulovať pomocou modulov a súkromných symbolov.

Triedy

Použitie tried môže poskytnúť viac objektovo orientovaný prístup k definovaniu odlišných typov. Hoci sú triedy v TypeScripte štrukturálne typované, ponúkajú jasnejšie oddelenie zodpovedností a môžu byť použité na vynútenie obmedzení prostredníctvom metód.

Knižnice ako `io-ts` alebo `zod`

Tieto knižnice poskytujú sofistikovanú validáciu typov za behu a môžu byť kombinované s 'branded types', aby sa zabezpečila bezpečnosť v čase kompilácie aj za behu.

Záver

TypeScript 'branded types' sú cenným nástrojom na zvýšenie typovej bezpečnosti a prehľadnosti kódu v štrukturálnom typovom systéme. Pridaním "značky" (brand) k typu môžete vynútiť nominálne typovanie a zabrániť náhodnému zamieňaniu štrukturálne podobných, ale logicky odlišných typov. Hoci 'branded types' prinášajú určitú zložitosť a réžiu, výhody zlepšenej typovej bezpečnosti a udržiavateľnosti kódu často prevážia nevýhody. Zvážte použitie 'branded types' v scenároch, kde potrebujete zabezpečiť, aby hodnota patrila ku konkrétnemu typu, bez ohľadu na jej štruktúru.

Pochopením princípov za štrukturálnym a nominálnym typovaním a aplikovaním osvedčených postupov uvedených v tomto článku môžete efektívne využiť 'branded types' na písanie robustnejšieho a udržiavateľnejšieho kódu v TypeScript. Od reprezentácie mien a ID až po vynucovanie doménovo-špecifických obmedzení, 'branded types' poskytujú flexibilný a mocný mechanizmus na zvýšenie typovej bezpečnosti vo vašich projektoch.

Pri práci s TypeScriptom preskúmajte rôzne techniky a knižnice dostupné na validáciu a vynucovanie typov. Zvážte použitie 'branded types' v spojení s knižnicami na validáciu za behu, ako sú io-ts alebo zod, aby ste dosiahli komplexný prístup k typovej bezpečnosti.

TypeScript Branded Types: Nominálne Typovanie v Štrukturálnom Systéme | MLOG